package studio.baka.satoripixeldungeon.items.wands;

import studio.baka.satoripixeldungeon.Assets;
import studio.baka.satoripixeldungeon.Badges;
import studio.baka.satoripixeldungeon.Dungeon;
import studio.baka.satoripixeldungeon.actors.Actor;
import studio.baka.satoripixeldungeon.actors.Char;
import studio.baka.satoripixeldungeon.actors.buffs.*;
import studio.baka.satoripixeldungeon.actors.hero.Hero;
import studio.baka.satoripixeldungeon.actors.hero.HeroClass;
import studio.baka.satoripixeldungeon.actors.hero.HeroSubClass;
import studio.baka.satoripixeldungeon.effects.MagicMissile;
import studio.baka.satoripixeldungeon.items.Item;
import studio.baka.satoripixeldungeon.items.bags.Bag;
import studio.baka.satoripixeldungeon.items.bags.MagicalHolster;
import studio.baka.satoripixeldungeon.items.rings.RingOfEnergy;
import studio.baka.satoripixeldungeon.items.weapon.melee.MagesStaff;
import studio.baka.satoripixeldungeon.items.weapon.melee.MahoStaff;
import studio.baka.satoripixeldungeon.mechanics.Ballistica;
import studio.baka.satoripixeldungeon.messages.Messages;
import studio.baka.satoripixeldungeon.scenes.CellSelector;
import studio.baka.satoripixeldungeon.scenes.GameScene;
import studio.baka.satoripixeldungeon.ui.QuickSlotButton;
import studio.baka.satoripixeldungeon.utils.GLog;
import com.watabou.noosa.audio.Sample;
import com.watabou.utils.Bundle;
import com.watabou.utils.Callback;
import com.watabou.utils.PointF;
import com.watabou.utils.Random;

import java.util.ArrayList;

//import com.sun.xml.internal.ws.api.message.Message;

public abstract class Wand extends Item {

    public static final String AC_ZAP = "ZAP";
    public static final String AC_CHARGE = "CHARGE";

    private static final float TIME_TO_ZAP = 1f;

    public int maxCharges = initialCharges();
    public int curCharges = maxCharges;
    public float partialCharge = 0f;

    protected Charger charger;

    private boolean curChargeKnown = false;

    public boolean curseInfusionBonus = false;

    public int curselevel = 0;

    private static final int USES_TO_ID = 10;
    private int usesLeftToID = USES_TO_ID;
    private float availableUsesToID = USES_TO_ID / 2f;

    protected int collisionProperties = Ballistica.MAGIC_BOLT;

    {
        defaultAction = AC_ZAP;
        usesTargeting = true;
        bones = true;
    }

    @Override
    public ArrayList<String> actions(Hero hero) {
        ArrayList<String> actions = super.actions(hero);
        if (curCharges > 0 || !curChargeKnown) {
            actions.add(AC_ZAP);
        }
        if (curCharges < maxCharges && hero.heroClass == HeroClass.MAHOU_SHOUJO) {
            actions.add(AC_CHARGE);
        }
        return actions;
    }

    @Override
    public void execute(Hero hero, String action) {

        super.execute(hero, action);

        if (action.equals(AC_ZAP)) {
            curUser = hero;
            curItem = this;
            GameScene.selectCell(zapper);
        } else if (action.equals(AC_CHARGE)) {
            curUser = hero;
            curItem = this;
            ForceCharge(hero);
        }
    }

    protected abstract void onZap(Ballistica attack);

    public abstract void onHit(MagesStaff staff, Char attacker, Char defender, int damage);

    public abstract void onHit(MahoStaff staff, Char attacker, Char defender, int damage);

    public void ForceCharge(Hero owner) {
        if (curCharges < maxCharges) {
            if (owner.mana >= manaRequirement()) {
                if (owner.subClass == HeroSubClass.DEVIL) {
                    GLog.w(Messages.get(this, "use_mana"));
                    owner.spend(0.25f);
                    owner.busy();
                    owner.sprite.operate(owner.pos);
                    gainCharge(2);
                    owner.mana -= manaRequirement();
                } else {
                    GLog.w(Messages.get(this, "use_mana"));
                    owner.spend(0.25f);
                    owner.busy();
                    owner.sprite.operate(owner.pos);
                    gainCharge(1);
                    owner.mana -= manaRequirement();
                }
            } else {
                GLog.w(Messages.get(this, "not_enough_mana"), owner.mana, owner.getMaxmana(), manaRequirement());
            }
        } else {
            GLog.w(Messages.get(this, "charge_full"), owner.mana, owner.getMaxmana(), manaRequirement());
        }
    }

    public boolean tryToZap(Hero owner, int target) {

        if (owner.buff(MagicImmune.class) != null) {
            GLog.w(Messages.get(this, "no_magic"));
            return false;
        }

        if (curCharges >= (cursed ? 1 : chargesPerCast())) {
            return true;
        } else {
            GLog.w(Messages.get(this, "fizzles"));
            return false;
        }
    }

    @Override
    public boolean collect(Bag container) {
        if (super.collect(container)) {
            if (container.owner != null) {
                if (container instanceof MagicalHolster)
                    charge(container.owner, MagicalHolster.HOLSTER_SCALE_FACTOR);
                else
                    charge(container.owner);
            }
            return true;
        } else {
            return false;
        }
    }

    public void gainCharge(float amt) {
        partialCharge += amt;
        while (partialCharge >= 1) {
            curCharges = Math.min(maxCharges, curCharges + 1);
            partialCharge--;
            updateQuickslot();
        }
    }

    public void charge(Char owner) {
        if (charger == null) charger = new Charger();
        charger.attachTo(owner);
    }

    public void charge(Char owner, float chargeScaleFactor) {
        charge(owner);
        charger.setScaleFactor(chargeScaleFactor);
    }

    protected void processSoulMark(Char target, int chargesUsed) {
        processSoulMark(target, level(), chargesUsed);
    }

    protected static void processSoulMark(Char target, int wandLevel, int chargesUsed) {
        if (target != Dungeon.hero &&
                Dungeon.hero.subClass == HeroSubClass.WARLOCK &&
                //standard 1 - 0.92^x chance, plus 7%. Starts at 15%
                Random.Float() > (Math.pow(0.92f, (wandLevel * chargesUsed) + 1) - 0.07f)) {
            SoulMark.prolong(target, SoulMark.class, SoulMark.DURATION + wandLevel);
        }
    }

    @Override
    public void onDetach() {
        stopCharging();
    }

    public void stopCharging() {
        if (charger != null) {
            charger.detach();
            charger = null;
        }
    }

    public void level(int value) {
        super.level(value);
        updateLevel();
    }

    @Override
    public Item identify() {

        curChargeKnown = true;
        super.identify();

        updateQuickslot();

        return this;
    }

    public void onHeroGainExp(float levelPercent, Hero hero) {
        if (!isIdentified() && availableUsesToID <= USES_TO_ID / 2f) {
            //gains enough uses to ID over 1 level
            availableUsesToID = Math.min(USES_TO_ID / 2f, availableUsesToID + levelPercent * USES_TO_ID / 2f);
        }
    }

    @Override
    public String info() {
        String desc = desc();

        desc += "\n\n" + statsDesc();

        if (cursed && cursedKnown) {
            desc += "\n\n" + Messages.get(Wand.class, "cursed");
        } else if (!isIdentified() && cursedKnown) {
            desc += "\n\n" + Messages.get(Wand.class, "not_cursed");
        }

        return desc;
    }

    public String statsDesc() {
        return Messages.get(this, "stats_desc");
    }

    @Override
    public boolean isIdentified() {
        return super.isIdentified() && curChargeKnown;
    }

    @Override
    public String status() {
        if (levelKnown) {
            return (curChargeKnown ? curCharges : "?") + "/" + maxCharges;
        } else {
            return null;
        }
    }

    @Override
    public int level() {
        if (!cursed && curseInfusionBonus) {
            curseInfusionBonus = false;
            updateLevel();
        }
        return super.level() + (curseInfusionBonus ? 1 : 0);
    }

    @Override
    public Item upgrade() {

        super.upgrade();

        if (Random.Int(3) == 0) {
            cursed = false;
        }

        updateLevel();
        curCharges = Math.min(curCharges + 1, maxCharges);
        updateQuickslot();

        return this;
    }

    @Override
    public Item degrade() {
        super.degrade();

        updateLevel();
        updateQuickslot();

        return this;
    }

    public void updateLevel() {
        maxCharges = Math.min(initialCharges() + level(), 10);
        curCharges = Math.min(curCharges, maxCharges);
    }

    protected int initialCharges() {
        return 2;
    }

    protected int manaRequirement() {
        return 5 + level();
    }

    protected int chargesPerCast() {
        return 1;
    }

    protected void fx(Ballistica bolt, Callback callback) {
        MagicMissile.boltFromChar(curUser.sprite.parent,
                MagicMissile.MAGIC_MISSILE,
                curUser.sprite,
                bolt.collisionPos,
                callback);
        Sample.INSTANCE.play(Assets.SND_ZAP);
    }

    public void staffFx(MagesStaff.StaffParticle particle) {
        particle.color(0xFFFFFF);
        particle.am = 0.3f;
        particle.setLifespan(1f);
        particle.speed.polar(Random.Float(PointF.PI2), 2f);
        particle.setSize(1f, 2f);
        particle.radiateXY(0.5f);
    }

    public void staffFx(MahoStaff.StaffParticle particle) {
        particle.color(0xFFFFFF);
        particle.am = 0.3f;
        particle.setLifespan(1f);
        particle.speed.polar(Random.Float(PointF.PI2), 2f);
        particle.setSize(1f, 2f);
        particle.radiateXY(0.5f);
    }

    public void wandUsed() {
        if (!isIdentified() && availableUsesToID >= 1) {
            availableUsesToID--;
            usesLeftToID--;
            if (usesLeftToID <= 0) {
                identify();
                GLog.p(Messages.get(Wand.class, "identify"));
                Badges.validateItemLevelAquired(this);
            }
        }

        curCharges -= cursed ? 1 : chargesPerCast();

        if (curUser.heroClass == HeroClass.MAGE) levelKnown = true;
        updateQuickslot();

        curUser.spendAndNext(TIME_TO_ZAP);
    }

    @Override
    public Item random() {
        //+0: 66.67% (2/3)
        //+1: 26.67% (4/15)
        //+2: 6.67%  (1/15)
        int n = 0;
        if (Random.Int(3) == 0) {
            n++;
            if (Random.Int(5) == 0) {
                n++;
            }
        }
        level(n);

        //30% chance to be cursed
        if (Random.Float() < 0.3f) {
            cursed = true;
        }

        return this;
    }

    @Override
    public int price() {
        int price = 75;
        if (cursed && cursedKnown) {
            price /= 2;
        }
        if (levelKnown) {
            if (level() > 0) {
                price *= (level() + 1);
            } else if (level() < 0) {
                price /= (1 - level());
            }
        }
        if (price < 1) {
            price = 1;
        }
        return price;
    }

    private static final String USES_LEFT_TO_ID = "uses_left_to_id";
    private static final String AVAILABLE_USES = "available_uses";
    private static final String CUR_CHARGES = "curCharges";
    private static final String CUR_CHARGE_KNOWN = "curChargeKnown";
    private static final String PARTIALCHARGE = "partialCharge";
    private static final String CURSE_INFUSION_BONUS = "curse_infusion_bonus";
    private static final String CURSE_LEVEL = "curselevel";

    @Override
    public void storeInBundle(Bundle bundle) {
        super.storeInBundle(bundle);
        bundle.put(USES_LEFT_TO_ID, usesLeftToID);
        bundle.put(AVAILABLE_USES, availableUsesToID);
        bundle.put(CUR_CHARGES, curCharges);
        bundle.put(CUR_CHARGE_KNOWN, curChargeKnown);
        bundle.put(PARTIALCHARGE, partialCharge);
        bundle.put(CURSE_INFUSION_BONUS, curseInfusionBonus);
        bundle.put(CURSE_LEVEL, curselevel);
    }

    @Override
    public void restoreFromBundle(Bundle bundle) {
        super.restoreFromBundle(bundle);
        usesLeftToID = bundle.getInt(USES_LEFT_TO_ID);
        availableUsesToID = bundle.getInt(AVAILABLE_USES);

        //pre-0.7.2 saves
        if (bundle.contains("unfamiliarity")) {
            usesLeftToID = Math.min(10, bundle.getInt("unfamiliarity"));
            availableUsesToID = USES_TO_ID / 2f;
        }
        curCharges = bundle.getInt(CUR_CHARGES);
        curChargeKnown = bundle.getBoolean(CUR_CHARGE_KNOWN);
        partialCharge = bundle.getFloat(PARTIALCHARGE);
        curseInfusionBonus = bundle.getBoolean(CURSE_INFUSION_BONUS);
        curselevel = bundle.getInt(CURSE_LEVEL);
    }

    @Override
    public void reset() {
        super.reset();
        usesLeftToID = USES_TO_ID;
        availableUsesToID = USES_TO_ID / 2f;
    }

    protected static CellSelector.Listener zapper = new CellSelector.Listener() {

        @Override
        public void onSelect(Integer target) {

            if (target != null) {

                //FIXME this safety check shouldn't be necessary
                //it would be better to eliminate the curItem static variable.
                final Wand curWand;
                if (curItem instanceof Wand) {
                    curWand = (Wand) Wand.curItem;
                } else {
                    return;
                }

                final Ballistica shot = new Ballistica(curUser.pos, target, curWand.collisionProperties);
                int cell = shot.collisionPos;

                if (target == curUser.pos || cell == curUser.pos) {
                    GLog.i(Messages.get(Wand.class, "self_target"));
                    //return;
                }

                curUser.sprite.zap(cell);

                //attempts to target the cell aimed at if something is there, otherwise targets the collision pos.
                if (Actor.findChar(target) != null)
                    QuickSlotButton.target(Actor.findChar(target));
                else
                    QuickSlotButton.target(Actor.findChar(cell));

                if (curWand.tryToZap(curUser, target)) {

                    curUser.busy();
                    Invisibility.dispel();

                    if (curWand.cursed) {
                        if (!curWand.cursedKnown) {
                            GLog.n(Messages.get(Wand.class, "curse_discover", curWand.name()));
                        }
                        CursedWand.cursedZap(curWand,
                                curUser,
                                new Ballistica(curUser.pos, target, Ballistica.MAGIC_BOLT),
                                curWand::wandUsed, ((Wand) curItem).curselevel);
                    } else {
                        curWand.fx(shot, () -> {
                            curWand.onZap(shot);
                            curWand.wandUsed();
                        });
                    }
                    curWand.cursedKnown = true;

                }

            }
        }

        @Override
        public String prompt() {
            return Messages.get(Wand.class, "prompt");
        }
    };

    public class Charger extends Buff {

        private static final float BASE_CHARGE_DELAY = 10f;
        private static final float SCALING_CHARGE_ADDITION = 40f;
        private static final float NORMAL_SCALE_FACTOR = 0.875f;

        private static final float CHARGE_BUFF_BONUS = 0.25f;

        float scalingFactor = NORMAL_SCALE_FACTOR;

        @Override
        public boolean attachTo(Char target) {
            super.attachTo(target);

            return true;
        }

        @Override
        public boolean act() {
            if (curCharges < maxCharges)
                recharge();

            if (target instanceof Hero) {
                Hero hero = (Hero) target;
                if (hero.heroClass == HeroClass.MAHOU_SHOUJO) {
                    partialCharge = 0;
                }
            }


            while (partialCharge >= 1 && curCharges < maxCharges) {
                partialCharge--;
                curCharges++;
                updateQuickslot();
            }

            if (curCharges == maxCharges) {
                partialCharge = 0;
            }

            spend(TICK);

            return true;
        }

        private void recharge() {
            int missingCharges = maxCharges - curCharges;
            missingCharges = Math.max(0, missingCharges);

            float turnsToCharge = (float) (BASE_CHARGE_DELAY
                    + (SCALING_CHARGE_ADDITION * Math.pow(scalingFactor, missingCharges)));

            LockedFloor lock = target.buff(LockedFloor.class);
            if (lock == null || lock.regenOn())
                partialCharge += (1f / turnsToCharge) * RingOfEnergy.wandChargeMultiplier(target);

            for (Recharging bonus : target.buffs(Recharging.class)) {
                if (bonus != null && bonus.remainder() > 0f) {
                    partialCharge += CHARGE_BUFF_BONUS * bonus.remainder();
                }
            }
        }

        public Wand wand() {
            return Wand.this;
        }

        public void gainCharge(float charge) {
            partialCharge += charge;
            while (partialCharge >= 1f) {
                curCharges++;
                partialCharge--;
            }
            curCharges = Math.min(curCharges, maxCharges);
            updateQuickslot();
        }

        private void setScaleFactor(float value) {
            this.scalingFactor = value;
        }
    }
}
